package log

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"time"
)

var _ = Target(&TargetFileRotate{})

//RotateConfig 滚动配置
type RotateConfig struct {
	Dir    string
	Prefix string
	Ext    string

	//seconds
	RotateDuring int64

	//bytes
	RotateSize int64
}

//TargetFileRotate file target with rotation.
type TargetFileRotate struct {
	cfg *RotateConfig

	levels []int
	using  int32

	file *os.File

	locker sync.Mutex

	lastRotateTime int64
	rotateDuring   int64

	stackSize  int64
	rotateSize int64
	sizeIndex  int64
}

//MakeTargetFileRotate make a TargetFileRotate instance.
func MakeTargetFileRotate(cfg *RotateConfig) (*TargetFileRotate, error) {
	t := &TargetFileRotate{
		cfg:    cfg,
		levels: append([]int{}, allLevels...),
		using:  0,

		file: nil,

		lastRotateTime: 0,
		rotateDuring:   cfg.RotateDuring,

		stackSize:  0,
		rotateSize: cfg.RotateSize,
		sizeIndex:  0,
	}

	now := time.Now()
	today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	t.lastRotateTime = now.Unix() - ((now.Unix() - today.Unix()) % cfg.RotateDuring)

	fn := t.genFileName(t.lastRotateTime)
	f, oldSize, err := t.openFile(fn)
	if nil != err {
		return nil, fmt.Errorf("can't open or create log file [%s]", fn)
	}
	t.file = f
	t.stackSize = oldSize

	return t, nil
}

//Name implement for Target.Name()
func (t *TargetFileRotate) Name() string {
	return "file." + t.cfg.Prefix
}

//IncrUsing implement for Target.IncrUsing()
func (t *TargetFileRotate) IncrUsing() {
	atomic.AddInt32(&t.using, 1)
}

//IsIgnore implement for Target.IsIgnore()
func (t *TargetFileRotate) IsIgnore(l int) bool {
	for _, s := range t.levels {
		if s == l {
			return false
		}
	}
	return true
}

//SetLevels set levels this target should output.
func (t *TargetFileRotate) SetLevels(ls []int) {
	t.levels = append([]int{}, ls...)
}

//Put implement for Target.Put()
func (t *TargetFileRotate) Put(msg string) {
	t.locker.Lock()
	defer t.locker.Unlock()

	//try trigger file rotate, +1 for '\n'
	t.tryRotateFile(int64(len(msg)) + 1)

	//output log message to file
	fmt.Fprintln(t.file, msg)
}

//Close close file
func (t *TargetFileRotate) Close() {
	if atomic.AddInt32(&t.using, -1) != 0 {
		return
	}

	t.locker.Lock()
	defer t.locker.Unlock()

	t.file.Sync()
	t.file.Close()
}

//genFileName generate file name as current status
func (t *TargetFileRotate) genFileName(ts int64) string {
	return fmt.Sprintf("%s.%s.%d.%s",
		t.cfg.Prefix, time.Unix(ts, 0).Format("2006-01-02-150405"), t.sizeIndex, t.cfg.Ext)
}

//tryRotateFile try rotate log file, must in locked block
func (t *TargetFileRotate) tryRotateFile(delta int64) {
	sizeFull := false
	timeFull := false

	//check size rotation
	if t.stackSize+delta >= t.cfg.RotateSize {
		sizeFull = true
	}

	//check time rotation
	now := time.Now()
	nowTs := now.Unix()
	if t.lastRotateTime+t.cfg.RotateDuring <= nowTs {
		timeFull = true
	}

	if !sizeFull && !timeFull {
		t.stackSize += delta
		return
	}

	if timeFull {
		rn := (nowTs - t.lastRotateTime) / t.cfg.RotateDuring
		t.lastRotateTime += t.cfg.RotateDuring * rn
		t.sizeIndex = 0
	} else if sizeFull {
		t.sizeIndex++
	}
	t.stackSize = delta

	//generate new file
	fn := t.genFileName(t.lastRotateTime)
	f, oldSize, err := t.openFile(fn)
	if nil != err {
		fmt.Printf("TargetFileRotate Error: can't open or create log file [%s]\n, %v", fn, err)
		return
	}

	//change file
	t.file.Sync()
	t.file.Close()
	t.file = f
	t.stackSize += oldSize
}

//openFile open new log file
func (t *TargetFileRotate) openFile(fn string) (*os.File, int64, error) {
	path := filepath.Join(t.cfg.Dir, fn)
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	if nil != err {
		return nil, 0, err
	}

	stat, err := f.Stat()
	if nil != err {
		return nil, 0, err
	}

	return f, stat.Size(), nil
}
